<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage core
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

namespace mg {

    use \Closure, \ReflectionFunction, \ReflectionClass, \ReflectionFunctionAbstract, \ReflectionMethod;

    interface Scope {

        const LAZY_SINGLETON = 0x00;
        const UNLAZY_SINGLETON = 0x01;
        const NO_SCOPE = 0x02;

        const SINGLETON = self :: LAZY_SINGLETON;
    }

    interface Provider {

        public function getInstance();

    }

    interface ContextConfiguration {

        public function configure(Binder $binder);

    }

    class VoidContext extends Context {

        public function __construct() {
            parent :: __construct(function(Binder $binder) { });
        }

    }

    class TransactionalContext extends Context {

        private $transactionals = null;
        private $context = null;

        public function __construct(Context $context) {
            $this->context = $context;
            $this->transactionals = array();
        }

        public function addTransactional(Transactional $transactional) {
            $this->transactionals[] = $transactional;
        }

        protected function resolve($className, $name = null) {
            return $this->context->resolve($className, $name);
        }

        public function __invoke(Closure $closure) {

            /* @var $transactional Transactional */
            foreach($this->transactionals as $transactional) {
                $transactional->begin();
            }

            $exception = null;

            $result = null;

            try {
                $result = $this->context->__invoke($closure);
            } catch(\Exception $e) {
                $exception = $e;
            }

            if($exception === null) {
                foreach($this->transactionals as $transactional) {
                    $transactional->commit();
                }
            } else {
                foreach($this->transactionals as $transactional) {
                    $transactional->rollback();
                }

                throw $exception;
            }

            return $result;
        }
    }

    class ConfiguredContext extends Context {

        public function __construct($arg0, $arg1 = null) {
            $contextConfiguration = null;

            if($arg0 instanceof Context) {
                if($arg1 === null || !($arg1 instanceof ContextConfiguration)) {
                    throw new Exception("No ContextConfiguration provided");
                }

                parent :: __construct($arg0, function(Binder $binder) use ($arg1) { $arg1->configure($binder); });
            } elseif($arg0 instanceof ContextConfiguration) {
                parent :: __construct(function(Binder $binder) use ($arg0) { $arg0->configure($binder); });
            } else {
                throw new Exception("No ContextConfiguration provided");
            }

        }
    }

    class Context {

        private static $stack = array();

        /**
         * @var Context
         */
        protected $parent = null;

        protected $bindings = null;

        protected $singletonScope = null;

        protected $depth = null;

        protected $unlazySingletons = null;

        public function __construct($arg0, $arg1=null) {
            if($arg0 instanceof Context) {
                if($arg1 === null) {
                    throw new Exception("No bind closure provided");
                }

                $this->parent = $arg0;
                $closure = $arg1;
            } else {
                $closure = $arg0;
            }

            $binder = new Binder();

            $closure($binder);

            $this->depth = 0;

            $this->bindings = $binder->getBindings();
            $this->unlazySingletons = $binder->getUnlazySingletons();

            $this->singletonScope = array();

        }

        /**
         * Check whether the given Context is a root Context. A root
         * Context doesn't have parenting Context.
         *
         * @return boolean
         */
        public function isRoot() {
            $this->parent === null;
        }

        public static function getContext() {
            $context = end(self :: $stack);

            if($context === false) {
                throw new Exception('Out of context');
            }

            return $context;
        }

        public static function in(Closure $closure) {
            $context = end(self :: $stack);

            if($context === false) {
                throw new Exception('Out of context');
            }

            return $context($closure);
        }

        public static function get($className) {
            $context = end(self :: $stack);

            if($context === false) {
                throw new Exception('Out of context');
            }

            return $context->getInstance($className);
        }

        public function createInstance($className) {
            $targetClass = new ReflectionClass($className);
            /* @var $constructor ReflectionMethod */
            $constructor = $targetClass->getConstructor();

            if($constructor !== null) {
                $executionParameters = $this->injectParameterValues($constructor);
                $result = $targetClass->newInstanceArgs($executionParameters);
            } else {
                $result = $targetClass->newInstance();
            }

            return $result;
        }

        public function getInstance($className, $name = null) {
            $result = $this->resolve(mb_strtolower($className), $name);

            if($result === null) {
                $result = $this->findInstance($className, $name);
            }

            return $result;
        }

        protected function findInstance($className, $name = null) {
            $targetClass = new ReflectionClass($className);

            // prefer resolve
            if($targetClass->hasMethod('resolveInstance')) {

                /* @var $resolveMethod ReflectionClass */
                $resolveMethod = $targetClass->getMethod('resolveInstance');
                $requiredFlags = ReflectionMethod :: IS_STATIC | ReflectionMethod :: IS_PUBLIC;

                if(($resolveMethod->getModifiers() & $requiredFlags) === $requiredFlags) {
                    return $className :: resolveInstance($name);
                }

            }

            return $this->createInstance($className, $name);
        }

        protected function resolveInstance($className, $name = null) {
            $targetClass = new ReflectionClass($className);

            if($targetClass->hasMethod('resolveInstance')) {

                /* @var $resolveMethod ReflectionClass */
                $resolveMethod = $targetClass->getMethod('resolveInstance');
                $requiredFlags = ReflectionMethod :: IS_STATIC | ReflectionMethod :: IS_PUBLIC;

                if(($resolveMethod->getModifiers() & $requiredFlags) === $requiredFlags) {
                    // unfortunately, we cannot use the invoke method as it throws away the advantage of lsb
                    return $className :: resolveInstance($name);
                }
            }
            return null;
        }
        /**
         * Resolve an instance of $className.
         *
         * @param string $className The name of the class to be resolved.
         * @return mixed An instance of $className or null if no instance could be resolved.
         */
        protected function resolve($className, $name = null) {
            // On Context request, return self
            if($className === 'mg\context') {
                return $this;
            }

            if(isset($this->singletonScope[$className])) {
                return $this->singletonScope[$className];
            }

            if(!isset($this->bindings[$className])) {
                if($this->parent === null) {
                    return null;
                } else {
                    return $this->parent->resolve($className);
                }
            }

            $binding = $this->bindings[$className];

            $isSingleton = false;

            $result = null;

            switch($binding['type']) {
                case 'closure' :
                    $result = $this->__invoke($binding['target']);

                    break;

                case 'provider' :

                    $result = $binding['target']->getInstance();

                    break;

                case 'class' :
                    $target = $binding['target'];

                    // If the classname refers to another bound classname, reresolve the instance
                    if($className !== $target && isset($this->bindings[$target])) {
                        $result = $this->resolve($target, $name);
                    } else {
                        $result = $this->findInstance($binding['target'], $name);
                    }

                    break;

                case 'instance' :

                    $result = $binding['target'];

                    $isSingleton = true;

                    break;
            }

            $isSingleton = $isSingleton || (isset($binding['scope']) && ($binding['scope'] === Scope :: UNLAZY_SINGLETON || $binding['scope'] === Scope :: LAZY_SINGLETON));

            if($isSingleton && ($result !== null)) {
                $this->singletonScope[$className] = $result;
            }

            return $result;
        }

        public function __invoke(Closure $closure) {
            self :: $stack[] = $this;

            $this->depth++;

            // initialize unlazy singletons at opening of scope
            if($this->depth === 1) {

                foreach($this->unlazySingletons as $className) {
                    $this->getInstance($className);
                }
            }

            $e = null;

            try {
                $reflect = new ReflectionFunction($closure);

                $executionParameters = $this->injectParameterValues($reflect);

                $result = $reflect->invokeArgs($executionParameters);
            } catch(\Exception $e) {
            }

            array_pop(self :: $stack);

            $this->depth--;

            if($this->depth === 0) {

                foreach($this->singletonScope as $key=>$value) {
                    unset($this->singletonScope[$key]);
                }
            }

            if($e) {
                throw $e;
            }

            return $result;
        }

        public function injectParameterValues(ReflectionFunctionAbstract $abstractFunction) {
            $parameters = $abstractFunction->getParameters();

            $executionParameters = array();

            /* @var $parameter ReflectionParameter */

            reset($parameters);

            while(($parameter = current($parameters)) !== false) {

                /* @var $class ReflectionClass */
                $class = $parameter->getClass();

                $executionParameter = null;

                if($class !== null) {
                    $executionParameter = $this->resolve(mb_strtolower($class->getName()), $parameter->getName());
                    if($executionParameter === null) {
                        $executionParameter = $this->resolveInstance($class->getName(), $parameter->getName());
                    }
                    if($executionParameter === null) {
                        if(!$parameter->allowsNull()) {
                            throw new Exception("Unresolved dependency to '{$class->getName()}'");
                        }
                    }
                } elseif($parameter->isDefaultValueAvailable()) {
                    $executionParameter = $parameter->getDefaultValue();
                } else {
                    throw new Exception("Parameter '{$parameter->getName()}' not resolvable");
                }

                $executionParameters[] = $executionParameter;
                next($parameters);
            }

            return $executionParameters;
        }
    }

    class Binder {

        private $bindings = null;

        private $unlazySingletons = null;

        public function __construct() {
            $this->bindings = array();
            $this->unlazySingletons = array();
        }

        /**
         *
         * @param $className
         * @return PartialBinder
         */
        public function bind($className) {
            return new PartialBinder($this, $className);
        }

        public function bindInScope($className, $scope) {
            $this->bindings[mb_strtolower($className)]['scope'] = $scope;

            if($scope === Scope :: UNLAZY_SINGLETON) {
                $this->unlazySingletons[] = mb_strtolower($className);
            }
        }

        public function bindToProvider($className, Provider $provider) {
            $this->addBinding(mb_strtolower($className), array('type' => 'provider', 'target' => $provider));
        }

        public function bindToClosure($className, Closure $closure) {
            $this->addBinding(mb_strtolower($className), array('type' => 'closure', 'target' => $closure));
        }

        public function bindToClass($className, $targetClassName) {
            $this->addBinding(mb_strtolower($className), array('type' => 'class', 'target' => mb_strtolower($targetClassName)));
        }

        public function bindToInstance($className, $instance) {
            $this->addBinding(mb_strtolower($className), array('type' => 'instance', 'target' => $instance));
        }

        protected function addBinding($className, array $binding) {
            if(isset($this->bindings[mb_strtolower($className)])) {
                throw new Exception("Class '{$className}' already bound.");
            }

            $this->bindings[mb_strtolower($className)] = $binding;
        }

        public function getUnlazySingletons() {
            return $this->unlazySingletons;
        }

        public function getBindings() {
            return $this->bindings;
        }
    }

    class ScopeBinder {

        /**
         *
         * @var mgBinder
         */
        private $binder = null;
        private $className = null;

        public function __construct(Binder $binder, $className) {
            $this->binder = $binder;
            $this->className = $className;
        }

        public function in($scope) {
            $this->binder->bindInScope($this->className, $scope);
        }
    }

    class PartialBinder {

        /**
         *
         * @var Binder
         */
        private $binder = null;
        private $className = null;

        public function __construct(Binder $binder, $className) {
            $this->binder = $binder;
            $this->className = $className;
        }

        /**
         *
         * @param $target
         * @return ScopeBinder
         */
        public function to($target) {
            if(is_string($target)) {
                $this->toClass($target);
            } elseif($target instanceof Provider) {
                $this->toProvider($target);
            } elseif($target instanceof Closure) {
                $this->toClosure($target);
            } else {
                throw new Exception("Invalid target {$target}");
            }

            return new ScopeBinder($this->binder, $this->className);
        }

        public function toClass($targetClassName) {
            $this->binder->bindToClass($this->className, $targetClassName);
        }

        public function toProvider(Provider $provider) {
            $this->binder->bindToProvider($this->className, $provider);
        }

        public function toClosure(Closure $closure) {
            $this->binder->bindToClosure($this->className, $closure);
        }

        public function toInstance($instance) {
            $this->binder->bindToInstance($this->className, $instance);
        }
    }

    interface Transactional {

        public function begin();

        public function commit();

        public function rollback();

    }
}
namespace {

    function get($className) {
        if($className[0] === '\\') {
            $className = mb_substr($className, 1);
        }

        return \mg\Context :: get($className);
    }

    function newInstance($className) {
        if($className[0] === '\\') {
            $className = mb_substr($className, 1);
        }

        return \mg\Context :: getContext()->createInstance($className);
    }

    function in(\Closure $closure) {
        return \mg\Context :: in($closure);
    }

    function void(\Closure $closure) {
        $emptyContext = new \mg\VoidContext();
        $emptyContext($closure);
    }
}